#include "stdafx.h"
#include "Material.h"
#include "Viewer.h"

/**
Record of materials registered within the material manager
*/
std::vector<Materials::tMaterial*> Materials::g_Materials = std::vector<Materials::tMaterial*>();

bool Materials::GetMaterialTexture(HMATERIAL pHandle, Render::tResource** pOut) {

	if(!pOut || !pHandle)
		return false;

	tMaterial* pMaterial = (tMaterial*)pHandle;
	if(pMaterial->type == SAMPLE_TEXTURE) {

		Render::tResource* pTexture = (Render::tResource*)pMaterial->pData;
		if(pTexture) {

			*pOut = pTexture;
			return true;
		}
	}

	*pOut = NULL;
	return false;
}

/**
Gets a material by key, or creates new material if no key match found

@param key unique id for the material
@param pOut pointer to material handler for client use
@return true if successful otherwise false
*/
bool Materials::GetMaterial(std::string key, HMATERIAL* pOut) {

	if(!pOut)
		return false;

	FOREACH(tMaterial*, i, Materials::g_Materials) {

		if( (*i)->key == key ) {

			tMaterial* pMatrial = (*i);
			if(*pOut == pMatrial)
				return true;
			else
				Release(*pOut);

			pMatrial->refCount ++;
			*pOut = pMatrial;
			return true;
		}

	}

	tMaterial* pMaterial = new tMaterial();
	pMaterial->key = key;
	pMaterial->type = 0;
	pMaterial->pData = NULL;
	pMaterial->refCount = 1;

	Materials::g_Materials.push_back(pMaterial);
	*pOut = pMaterial;
	return true;
}

/**
Returns the size in bytes of a material data structure that is associated with a material type

@param type the type of this material (ie checker, texture map, ...) to return the size of
@return the size in bytes of the specified material type. O if a failure occoured
*/
uint Materials::getTypeSize(uint type) {

	switch(type) {

		case SAMPLE_TEXTURE:
			return sizeof(DataStructs::tTextureFile);

		case SAMPLE_WOOD:
			return sizeof(DataStructs::tWood);

		case SAMPLE_NOISE:
			return sizeof(DataStructs::tNoise);

		case SAMPLE_GRID:
			return sizeof(DataStructs::tGrid);

		case SAMPLE_RAMP:
			return sizeof(DataStructs::tRamp);
		
		case SAMPLE_CHECKER:
			return sizeof(DataStructs::tChecker);
	}
	return 0;
}

/**
Initalizes a material datum within the material manager

@param type the type of this material (ie checker, texture map, ...)
@param pData the data that can be cast accoriding to the respective type. Memory is copied so pointer is not owned by this function.
@param pMaterial pointer to the data structure that is being populated
@return true if successful otherwise false
*/
#include "..\Render\Render.h"
bool Materials::initalizeMaterial(uint type, byte* pData, tMaterial *pMaterial) {

	char* pProceduralData = NULL;

	try {

		if(!pData || !pMaterial)
			throw tException("Failed to create material");
		
		if(type == SAMPLE_TEXTURE) {

			
			DataStructs::tTextureFile* pHeader = (DataStructs::tTextureFile*)pData;
			byte* pReader = pData + sizeof(DataStructs::tTextureFile);
			std::string filename = CViewer::GetFilename(std::string(pHeader->filename.Buffer));;
			FILE* pFile = fopen(filename.c_str(), "wb");
			if(pFile) {

				fwrite(pReader, pHeader->size, 1, pFile);
				fclose(pFile);

				Render::Singleton()->CreateTexture(filename, (Render::tResource**)&pProceduralData);
			}
			pMaterial->pData = pProceduralData;
		}
		else {

		if(pMaterial->type != type || !pMaterial->pData) {

			switch(type) {

				case SAMPLE_WOOD:
					pProceduralData = (char*)new DataStructs::tWood();
					break;

				case SAMPLE_NOISE:
					pProceduralData = (char*)new DataStructs::tNoise();
					break;

				case SAMPLE_GRID:
					pProceduralData = (char*)new DataStructs::tGrid();
					break;

				case SAMPLE_RAMP:
					pProceduralData = (char*)new DataStructs::tRamp();
					break;

				case SAMPLE_CHECKER:
					pProceduralData = (char*)new DataStructs::tChecker();
					break;

				default:
					throw tException("Material type: %i unsupported", type);
			}

			if(pMaterial->pData)
				delete pMaterial->pData;

			pMaterial->pData = pProceduralData;
		}
			memcpy(pMaterial->pData, pData, getTypeSize(type));
		}

		pMaterial->type = type;
		return true;

	}catch(tException e) {

		if(pProceduralData)
			delete pProceduralData;

		std::string message;
		tException::GetMessage(&e, message);
		return false;
	}
}

/**
Allows controlled client specification and modification of a material's type and data.

@param type the type of this material (ie checker, texture map, ...)
@param pData the data that can be cast accoriding to the respective type. Memory is copied so pointer is not owned by this function.
@param key the key that is unqiue to a/the material
@return true if successful otherwise false
*/
bool Materials::UpdateMaterial(uint materialType, byte* pSamplerData, HMATERIAL pHandle) {

	try {

		if(!pHandle || !pSamplerData) 
			throw tException();

		FOREACH(tMaterial*, i, Materials::g_Materials) {

			tMaterial* pMaterial = (*i);
			if(pMaterial == pHandle) {

				if(!initalizeMaterial(materialType, pSamplerData, pMaterial))
					throw tException("Failed to update material");
				break;
			}
			else if(i == Materials::g_Materials.end())
				return false;
		}

		return true;

	}catch(tException e) {

		std::string message;
		tException::GetMessage(&e, message);
		return false;
	}
}

/**
Allows controlled client deallocation of a material instance. If found the materail ref count is decremented

@param key identifier of material to release.
@return true if successful otherwise false
*/
bool Materials::Release(std::string key) {

	FOREACH(tMaterial*, i, Materials::g_Materials) {

		if( (*i)->key == key ) {

			tMaterial* pMatrial = (*i);
			pMatrial->refCount --;
			if(pMatrial->refCount == 0) {

				Materials::g_Materials.erase(i);
				if(pMatrial->type == SAMPLE_TEXTURE) {
					Render::Singleton()->ReleaseResource((Render::tResource**)&pMatrial);
				}
				else {
					if(pMatrial->pData)
						delete [] pMatrial->pData;
					delete pMatrial;
				}
			}

			return true;
		}
	}

	return false;
}

/**
Allows controlled client deallocation of a material instance. If found the materail ref count is decremented.
The handle will no longer be valid after calling this function

@param handle handle of material to release.
@return true if successful otherwise false
*/
bool Materials::Release(HMATERIAL handle) {

	if(!handle)
		return false;

	FOREACH(tMaterial*, i, Materials::g_Materials) {

		if( (*i) == handle) {

			tMaterial* pMatrial = (*i);
			pMatrial->refCount --;
			if(pMatrial->refCount == 0) {

				Materials::g_Materials.erase(i);
				if(pMatrial->type == SAMPLE_TEXTURE) {
					Render::Singleton()->ReleaseResource((Render::tResource**)&pMatrial);
				}
				else {
					if(pMatrial->pData)
						delete [] pMatrial->pData;
					delete pMatrial;
				}
			}
			return true;
		}
	}

	return false;
}

/**
Allows a material to be stored on disk for subsequent recall

@param filename name to store the material under
@param handle handle to the material to store
@return true if successful otherwise false
*/
bool Materials::WriteToDisk(const std::string& filename, HMATERIAL handle) {

	FILE* pFile = NULL;

	try {

		if(!handle || filename.length() == 0)
			throw tException();

		FOREACH(tMaterial*, i, Materials::g_Materials) {

			tMaterial* pMaterial = (*i);
			if(pMaterial == handle) {

				uint dataSize = getTypeSize(pMaterial->type);
				if(dataSize == 0)
					throw tException("Invalid material type");

				if(!pMaterial->pData)
					throw tException("Invalid material data");

				pFile = fopen(filename.c_str(), "wb");
				if(!pFile)
					throw tException("Failed to open file: %s", filename.c_str());

				fwrite(&pMaterial->type, sizeof(uint), 1, pFile);
				fwrite(&pMaterial->pData, sizeof(byte), dataSize, pFile);
				fclose(pFile);
				return true;
			}
		}

		throw tException("Material handle not found");
		return false;

	}catch(tException e) {

		if(pFile)
			fclose(pFile);
		
		std::string message;
		tException::GetMessage(&e, message);
		return false;
	}
}

/**
Allows a material to be read from disk from previous storage

@param filename name of the material to recall
@param handle address of the material handle that will be used by the client
@return true if successful otherwise false
*/
bool Materials::ReadFromDisk(const std::string& filename, HMATERIAL* pOut) {
	
	FILE* pFile = NULL;
	HMATERIAL hMaterial = NULL;
	byte* pData = NULL;

	try {

		if(!pOut || filename.length() == 0)
			throw tException();

		/*
		Attempt to release a handle if it exists, don't worry if this fails as pointer probably junk to nothing
		*/
		if(*pOut)
			Release(*pOut);

		/*if(!GetMaterial(&hMaterial))
			throw tException("Failed to create material");*/

		pFile = fopen(filename.c_str(), "wb");
		if(!pFile)
			throw tException("Failed to open file: %s", filename.c_str());

		uint type = 0;
		fread(&type, sizeof(uint), 1, pFile);
		
		uint size = getTypeSize(type);
		if(size == 0)
			throw tException();

		pData = new byte [size];
		fread(pData, sizeof(byte), size, pFile);
		fclose(pFile);

		if(!initalizeMaterial(type, pData, (tMaterial*)hMaterial))
			throw tException();

		return true;

	}catch(tException e) {

		if(pFile)
			fclose(pFile);
		
		std::string message;
		tException::GetMessage(&e, message);
		return false;
	}
}

/**
Attempts to retrieve the type of the specified material handle

@param handle the material handle to check
@param pOut pointer to varaible that stores the material type if it can be determined
@return true if successful otherwise false
*/
bool Materials::GetType(HMATERIAL handle, uint* pOut) {

	if(handle && pOut) {

		tMaterial* pMaterial = (tMaterial*)handle;
		if(pMaterial->type < SAMPLE_TEXTURE || pMaterial->type > SAMPLE_RESERVED1)
			return false;

		*pOut = pMaterial->type;
		return true;
	}

	return false;
}

int ChannelToStage(const std::string& name) {

	if(name == "lv_color")
		return 0;
	if(name == "lv_normal")
		return 1;
	if(name == "lv_aux")
		return 2;

	return 0;
}

/**
Interprets a material handle and takes care of the shader binding process. The function first examines
the type of the handle, and then extracts information from it in preperation for binding to the associated
shader. The 'binding' of data for use in the shaders execution also occours within this function. The function
assumes the the passed shader has already been bound to the device for rendering

@param pShader the shader object to which parameters of the material will be bound
@param channel the channel within the shader that this material should be bound
@param handle the material handle that provides access to specific material data
@return true if successful otherwise false
*/
bool Materials::ApplyToShader(Render::tShader *pShader, const std::string& channel, HMATERIAL handle) {

	try {

		if(!pShader || channel.length() == 0 || !handle)
			throw tException();
		
		tMaterial* pMaterial = (tMaterial*)handle;
		/*
		I treat texture maps as a special case because they are currently associated with other data resources
		*/
		if(pMaterial->type == SAMPLE_TEXTURE) {
			
			Render::tResource* pTexture = (Render::tResource*)pMaterial->pData;
			Render::Singleton()->SetShaderTexture(ChannelToStage(channel), pTexture);
		}
		else {

			char symbolBuffer[128] = {0};
			char structBuffer[64] = {0};
			char fieldBuffer[64] = {0};
			#define SET_SHADER_SCALAR_PARAMETER(field, data) \
				sprintf(symbolBuffer, "%s@%s", structBuffer, field); \
				Render::Singleton()->SetShaderFloat(symbolBuffer, data);
			#define SET_SHADER_VECTOR_PARAMETER(field, data) \
				sprintf(symbolBuffer, "%s@%s", structBuffer, field); \
				Render::Singleton()->SetShaderVector(symbolBuffer, data);

			switch(pMaterial->type) {
				/*
				Bind wood parameters
				*/
				case SAMPLE_WOOD: {

					DataStructs::tWood* pWood = (DataStructs::tWood*)pMaterial->pData;
					sprintf(structBuffer, "woodParam_%s", channel.c_str());

					SET_SHADER_SCALAR_PARAMETER("age_count", pWood->ageCount);
					SET_SHADER_SCALAR_PARAMETER("age_spacing", pWood->ageSpacing);
					SET_SHADER_VECTOR_PARAMETER("color_a", &D3DXVECTOR4(pWood->color_a.x, pWood->color_a.y, pWood->color_a.z, 0.0f));
					SET_SHADER_VECTOR_PARAMETER("color_b", &D3DXVECTOR4(pWood->color_b.x, pWood->color_b.y, pWood->color_b.z, 0.0f));
					break;
				};

				/*
				Bind noise parameters
				*/
				case SAMPLE_NOISE: {

					DataStructs::tNoise* pNoise = (DataStructs::tNoise*)pMaterial->pData;
					sprintf(structBuffer, "noiseParam_%s", channel.c_str());

					SET_SHADER_SCALAR_PARAMETER("amplitude", pNoise->amplitude);
					SET_SHADER_SCALAR_PARAMETER("octaves", pNoise->octaves);
					SET_SHADER_SCALAR_PARAMETER("threshold", pNoise->offset);
					SET_SHADER_SCALAR_PARAMETER("frequency", pNoise->scale);
					break;
				};

				/*
				Bind grid parameters
				*/
				case SAMPLE_GRID: {

					DataStructs::tGrid* pGrid = (DataStructs::tGrid*)pMaterial->pData;
					sprintf(structBuffer, "gridParam_%s", channel.c_str());

					SET_SHADER_SCALAR_PARAMETER("amplitude", pGrid->contrast);
					SET_SHADER_SCALAR_PARAMETER("height", pGrid->height);
					SET_SHADER_SCALAR_PARAMETER("width", pGrid->width);
					SET_SHADER_VECTOR_PARAMETER("color_a", &D3DXVECTOR4(pGrid->color_a.x, pGrid->color_a.y, pGrid->color_a.z, 0.0f));
					SET_SHADER_VECTOR_PARAMETER("color_b", &D3DXVECTOR4(pGrid->color_b.x, pGrid->color_b.y, pGrid->color_b.z, 0.0f));
					break;
				};

				/*
				Bind ramp parameters
				*/
				case SAMPLE_RAMP: {

					DataStructs::tRamp* pRamp = (DataStructs::tRamp*)pMaterial->pData;
					sprintf(structBuffer, "rampParam_%s", channel.c_str());

					SET_SHADER_SCALAR_PARAMETER("wave", pRamp->wave);
					break;
				};

				/*
				Bind checker parameters
				*/
				case SAMPLE_CHECKER: {

					DataStructs::tChecker* pChecker = (DataStructs::tChecker*)pMaterial->pData;
					sprintf(structBuffer, "checkerParam_%s", channel.c_str());

					SET_SHADER_SCALAR_PARAMETER("contrast", pChecker->contrast);
					SET_SHADER_VECTOR_PARAMETER("color_a", &D3DXVECTOR4(pChecker->color_a.x, pChecker->color_a.y, pChecker->color_a.z, 0.0f));
					SET_SHADER_VECTOR_PARAMETER("color_b", &D3DXVECTOR4(pChecker->color_b.x, pChecker->color_b.y, pChecker->color_b.z, 0.0f));
					break;
				};

				default:
					throw tException("Unknown material type");
			}
		}
		return true;
	}
	catch(tException e) {

		std::string message;
		tException::GetMessage(&e, message);
		return false;
	}
}
